// License Notification
//======================================================
// Copyright (c) 2010 Channel Intelligence Inc.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// ======================================================




using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using CIP4.Common;

namespace CIP4.RulesEngine
{
    public class ArithmeticExpressionType : ExpressionType
    {
        public static ArithmeticOperatorsEnum ArithmeticOperatorsEnumFromString(string value)
        {
            if (string.Compare(value, "DIVIDE", true) == 0)
                return ArithmeticOperatorsEnum.DIVIDE;
            else if (string.Compare(value, "EXPON", true) == 0)
                return ArithmeticOperatorsEnum.EXPON;
            else if (string.Compare(value, "MINUS", true) == 0)
                return ArithmeticOperatorsEnum.MINUS;
            else if (string.Compare(value, "PLUS", true) == 0)
                return ArithmeticOperatorsEnum.PLUS;
            else if (string.Compare(value, "TIMES", true) == 0)
                return ArithmeticOperatorsEnum.TIMES;
            else
                throw new ArgumentException(string.Format("{0} is not a valid ArithmeticOperatorEnum value", value));
        }

        ArithmeticOperatorsEnum m_ExpressionOperator;

        public ArithmeticOperatorsEnum ExpressionOperator
        {
            get { return m_ExpressionOperator; }
            set { m_ExpressionOperator = value; }
        }

        public override CIP4PrimitiveType UnderlyingType
        {
            get { return CIP4PrimitiveType.CIP4Decimal; }
        }

        public ArithmeticExpressionType()
        {
        }

        public ArithmeticExpressionType(ArithmeticOperatorsEnum expressionOperator, List<CIValueType> expressionOperands)
        {
            m_ExpressionOperator = expressionOperator;
            Operands.SetOperands(expressionOperands);
        }

        public ArithmeticExpressionType(XElement element)
            : base(element)
        {
            XAttribute operatorAtt = element.Attribute("Operator");
            if (operatorAtt != null)
                ExpressionOperator = ArithmeticOperatorsEnumFromString(operatorAtt.Value);
        }

        public override CIValueType Clone()
        {
            return new ArithmeticExpressionType(m_ExpressionOperator, Operands.Select(val => val.Clone()).ToList());
        }

        public override void XmlFor(XmlWriter xwrtr, bool writeNamespace)
        {
            xwrtr.WriteStartElement("ArithmeticExpression");
            if (writeNamespace)
                xwrtr.WriteAttributeString("xmlns", RuleXmlNS);
            writeNamespace = false;
            xwrtr.WriteAttributeString("Operator", ExpressionOperator.ToString());
            foreach (CIValueType val in Operands)
                val.XmlFor(xwrtr, writeNamespace);
            xwrtr.WriteEndElement();
        }
        public Type ResultType(IDataPacketType data)
        {
            switch (ExpressionOperator)
            {
                case ArithmeticOperatorsEnum.DIVIDE:
                    {
                        object op1 = Operands[0].Evaluate(data);
                        object op2 = Operands[1].Evaluate(data);
                        // if neither is double and at least one is decimal then return decimal otherwise return double
                        if (!(op1 is double || op2 is double) && (op1 is decimal || op2 is decimal))
                            return typeof(Decimal);
                        else
                            return typeof(Double);
                    }
                case ArithmeticOperatorsEnum.EXPON:
                    {
                        object op1 = Operands[0].Evaluate(data);
                        object op2 = Operands[1].Evaluate(data);
                        // if both are integer and the exponent is positive return integer otherwise return double
                        if (op1 is long && op2 is long && (long)op2 > 0)
                            return typeof(Int64);
                        else
                            return typeof(Double);
                    }
                case ArithmeticOperatorsEnum.TIMES:
                    {
                        object op1 = Operands[0].Evaluate(data);
                        object op2 = Operands[1].Evaluate(data);
                        if (op1 is double || op2 is double)
                            return typeof(Double);
                        else if (op1 is decimal || op2 is decimal)
                            return typeof(Decimal);
                        else
                            return typeof(Int64);
                    }
                case ArithmeticOperatorsEnum.PLUS:
                    {
                        object op1 = Operands[0].Evaluate(data);
                        object op2 = Operands[1].Evaluate(data);
                        if (op1 is double || op2 is double)
                            return typeof(Double);
                        else if (op1 is decimal || op2 is decimal)
                            return typeof(Decimal);
                        else
                            return typeof(Int64);
                    }
                case ArithmeticOperatorsEnum.MINUS:
                    {
                        object op1 = Operands[0].Evaluate(data);
                        object op2 = Operands[1].Evaluate(data);
                        if (op1 is double || op2 is double)
                            return typeof(Double);
                        else if (op1 is decimal || op2 is decimal)
                            return typeof(Decimal);
                        else
                            return typeof(Int64);
                    }
                default:
                    throw new ApplicationException("unexpected arithmetic operator type encountered: working on " + ToString());
            }
        }
        public override object Evaluate(IDataPacketType data)
        {
            if (data != null && data.PacketStats != null)
                data.PacketStats.ArithmeticOperatorsCount[(int)ExpressionOperator] += 1;
            switch (ExpressionOperator)
            {
                case ArithmeticOperatorsEnum.DIVIDE:
                    {
                        object op1 = Operands[0].Evaluate(data);
                        object op2 = Operands[1].Evaluate(data);
                        // if neither is double and at least one is decimal then return decimal otherwise return double
                        if (!(op1 is double || op2 is double) && (op1 is decimal || op2 is decimal))
                            return System.Convert.ToDecimal(op1) / System.Convert.ToDecimal(op2);
                        else
                            return System.Convert.ToDouble(op1) / System.Convert.ToDouble(op2);
                    }
                case ArithmeticOperatorsEnum.EXPON:
                    {
                        object op1 = Operands[0].Evaluate(data);
                        object op2 = Operands[1].Evaluate(data);
                        // if both are integer and the exponent is positive return integer otherwise return double
                        if (op1 is long && op2 is long && (long)op2 > 0)
                            return System.Convert.ToInt64(op1) ^ System.Convert.ToInt64(op2);
                        else
                            return Math.Pow(System.Convert.ToDouble(op1), System.Convert.ToDouble(op2));
                    }
                case ArithmeticOperatorsEnum.TIMES:
                    {
                        object op1 = Operands[0].Evaluate(data);
                        object op2 = Operands[1].Evaluate(data);
                        if (op1 is double || op2 is double)
                            return System.Convert.ToDouble(op1) * System.Convert.ToDouble(op2);
                        else if (op1 is decimal || op2 is decimal)
                            return System.Convert.ToDecimal(op1) * System.Convert.ToDecimal(op2);
                        else
                            return System.Convert.ToInt64(op1) * System.Convert.ToInt64(op2);
                    }
                case ArithmeticOperatorsEnum.PLUS:
                    {
                        object op1 = Operands[0].Evaluate(data);
                        object op2 = Operands[1].Evaluate(data);
                        if (op1 is double || op2 is double)
                            return System.Convert.ToDouble(op1) + System.Convert.ToDouble(op2);
                        else if (op1 is decimal || op2 is decimal)
                            return System.Convert.ToDecimal(op1) + System.Convert.ToDecimal(op2);
                        else
                            return System.Convert.ToInt64(op1) + System.Convert.ToInt64(op2);
                    }
                case ArithmeticOperatorsEnum.MINUS:
                    {
                        object op1 = Operands[0].Evaluate(data);
                        object op2 = Operands[1].Evaluate(data);
                        if (op1 is double || op2 is double)
                            return System.Convert.ToDouble(op1) - System.Convert.ToDouble(op2);
                        else if (op1 is decimal || op2 is decimal)
                            return System.Convert.ToDecimal(op1) - System.Convert.ToDecimal(op2);
                        else
                            return System.Convert.ToInt64(op1) - System.Convert.ToInt64(op2);
                    }
                default:
                    throw new ApplicationException("unexpected arithmetic operator type encountered: working on " + ToString());
            }
        }

        private bool SetCheck(IDataPacketType data)
        {
            if (Operands.OfType<SetAtomType>().Any())
            {
                data.Logger(MajorEnum.Error, MinorEnum.ExpressionStructure, "A boolean expression using the {0} operator cannot have a SetAtomType as a parameter", new object[] { ExpressionOperator.ToString(), this.ToString() });
                return false;
            }

            return true;
        }

        public override bool Check(IDataPacketType data)
        {
            bool result = SetCheck(data);

            LoggerDelegate localLogger = delegate(MajorEnum status, MinorEnum minorStatus, string message, object[] paras)
            {
                if (status == MajorEnum.Error)
                    result = false;
                return data.Logger(status, minorStatus, message, paras);
            };

            switch (ExpressionOperator)
            {
                case ArithmeticOperatorsEnum.DIVIDE:
                case ArithmeticOperatorsEnum.EXPON:
                case ArithmeticOperatorsEnum.MINUS:
                case ArithmeticOperatorsEnum.PLUS:
                case ArithmeticOperatorsEnum.TIMES:
                    {
                        if (Operands.Count > 2)
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, 
                                "A logical expression using the {0} operator cannot have more than two operands: {1}", new object[] { ExpressionOperator.ToString(), this.ToString()});
                        else if (Operands.Count < 2)
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, 
                                "A logical expression using the {0} operator must have exactly two operands: {1}", new object[] { ExpressionOperator.ToString(), this.ToString()});
                        else if (!((Operands[0] is ArithmeticAtomType || Operands[0] is ArithmeticExpressionType) &&
                            (Operands[1] is ArithmeticAtomType || Operands[1] is ArithmeticExpressionType)))
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, 
                                "The operands for the {0} operator must both be arithmetic: {1}", new object[] { ExpressionOperator.ToString(), this.ToString()});
                        else
                        {
                            Operands[0].Check(data);
                            Operands[1].Check(data);
                        }
                        break;
                    }
                default:
                    {
                        localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, 
                            "{0} is not a recognized arithmetic operator in {1}", new object[] { ExpressionOperator.ToString(), this.ToString() });
                        break;
                    }
            }
            return result;
        }

        public override string ToString(ExpressionFormatEnum format)
        {switch (format)
            {
                case ExpressionFormatEnum.javascript:
                    return ToJavaScriptString();
                default:
                    return ToString();
            }
        }

        public override string ToString()
        {
            string leftOp = Operands.Count > 0? Operands[0].ToString() : "InvalidOPerand";
            string rightOp = Operands.Count > 1 ? Operands[1].ToString() : "InvalidOPerand";
            switch (ExpressionOperator)
            {
                case ArithmeticOperatorsEnum.PLUS:
                    return "(" + leftOp + ")+(" + rightOp + ")"; 
                case ArithmeticOperatorsEnum.MINUS:
                    return "(" + leftOp + ")-(" + rightOp + ")"; 
                case ArithmeticOperatorsEnum.DIVIDE:
                    return "(" + leftOp + ")/(" + rightOp + ")"; 
                case ArithmeticOperatorsEnum.TIMES:
                    return "(" + leftOp + ")*(" + rightOp + ")"; 
                case ArithmeticOperatorsEnum.EXPON:
                    return "(" + leftOp + ")^(" + rightOp + ")"; 
                default:
                    return "(" + leftOp + ") InvalidOperator (" + rightOp + ")"; 
            }
        }

        private string ToJavaScriptString()
        {
            if (Operands.Count != 2)
                throw new InvalidExpressionException(Operands.Count + "operands for ArithmeticExpressionType.");
            string leftOp = Operands[0].ToString(ExpressionFormatEnum.javascript);
            string rightOp = Operands[1].ToString(ExpressionFormatEnum.javascript);
            switch (ExpressionOperator)
            {
                case ArithmeticOperatorsEnum.PLUS:
                    return "(" + leftOp + ")+(" + rightOp + ")";
                case ArithmeticOperatorsEnum.MINUS:
                    return "(" + leftOp + ")-(" + rightOp + ")";
                case ArithmeticOperatorsEnum.DIVIDE:
                    return "(" + leftOp + ")/(" + rightOp + ")";
                case ArithmeticOperatorsEnum.TIMES:
                    return "(" + leftOp + ")*(" + rightOp + ")";
                case ArithmeticOperatorsEnum.EXPON:
                    return "Math.pow(" + leftOp + "," + rightOp + ")";
                default:
                    throw new InvalidExpressionException("Unhandled operator: " + ExpressionOperator);
            }
        }
    }
}
